<?php

if (!class_exists('K12Serv_API_Client', false)) {
    if (file_exists(dirname(__FILE__).'/K12Serv_API_Client.class.php')) require_once dirname(__FILE__).'/K12Serv_API_Client.class.php';
    elseif (class_exists('K12Serv_Loader', false)) K12Serv_Loader::loadClass('K12Serv_API_Client');
    else throw new Exception('请先加载类 K12Serv_API_Client。');
}

/**
 * 存储服务 API 客户端类。
 *
 * @author Nio Xiao
 * @package K12S3
 */
class K12S3_API_Client extends K12Serv_API_Client
{
    /**
     * 命令行 file 所在位置。
     */
    const BIN_FILE = '/usr/bin/file';

    /**
     * 扩展名与 mime type 的对应关系数组。
     * @var array
     */
    private static $mime_types = array (
      '323' => 'text/h323',
      'acx' => 'application/internet-property-stream',
      'ai' => 'application/postscript',
      'aif' => 'audio/x-aiff',
      'aifc' => 'audio/x-aiff',
      'aiff' => 'audio/x-aiff',
      'asf' => 'video/x-ms-asf',
      'asr' => 'video/x-ms-asf',
      'asx' => 'video/x-ms-asf',
      'au' => 'audio/basic',
      'avi' => 'video/x-msvideo',
      'axs' => 'application/olescript',
      'bas' => 'text/plain',
      'bcpio' => 'application/x-bcpio',
      'bin' => 'application/octet-stream',
      'bmp' => 'image/bmp',
      'c' => 'text/plain',
      'cat' => 'application/vnd.ms-pkiseccat',
      'cdf' => 'application/x-netcdf',
      'cer' => 'application/x-x509-ca-cert',
      'class' => 'application/octet-stream',
      'clp' => 'application/x-msclip',
      'cmx' => 'image/x-cmx',
      'cod' => 'image/cis-cod',
      'cpio' => 'application/x-cpio',
      'crd' => 'application/x-mscardfile',
      'crl' => 'application/pkix-crl',
      'crt' => 'application/x-x509-ca-cert',
      'csh' => 'application/x-csh',
      'css' => 'text/css',
      'dcr' => 'application/x-director',
      'der' => 'application/x-x509-ca-cert',
      'dir' => 'application/x-director',
      'dll' => 'application/x-msdownload',
      'dms' => 'application/octet-stream',
      'doc' => 'application/msword',
      'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
      'dot' => 'application/msword',
      'dvi' => 'application/x-dvi',
      'dxr' => 'application/x-director',
      'eps' => 'application/postscript',
      'etx' => 'text/x-setext',
      'evy' => 'application/envoy',
      'exe' => 'application/octet-stream',
      'fif' => 'application/fractals',
      'flr' => 'x-world/x-vrml',
      'gif' => 'image/gif',
      'gtar' => 'application/x-gtar',
      'gz' => 'application/x-gzip',
      'h' => 'text/plain',
      'hdf' => 'application/x-hdf',
      'hlp' => 'application/winhlp',
      'hqx' => 'application/mac-binhex40',
      'hta' => 'application/hta',
      'htc' => 'text/x-component',
      'htm' => 'text/html',
      'html' => 'text/html',
      'htt' => 'text/webviewhtml',
      'ico' => 'image/x-icon',
      'ief' => 'image/ief',
      'iii' => 'application/x-iphone',
      'ins' => 'application/x-internet-signup',
      'isp' => 'application/x-internet-signup',
      'jfif' => 'image/pipeg',
      'jpe' => 'image/jpeg',
      'jpeg' => 'image/jpeg',
      'jpg' => 'image/jpeg',
      'js' => 'application/x-javascript',
      'latex' => 'application/x-latex',
      'lha' => 'application/octet-stream',
      'lsf' => 'video/x-la-asf',
      'lsx' => 'video/x-la-asf',
      'lzh' => 'application/octet-stream',
      'm13' => 'application/x-msmediaview',
      'm14' => 'application/x-msmediaview',
      'm3u' => 'audio/x-mpegurl',
      'man' => 'application/x-troff-man',
      'mdb' => 'application/x-msaccess',
      'me' => 'application/x-troff-me',
      'mht' => 'message/rfc822',
      'mhtml' => 'message/rfc822',
      'mid' => 'audio/mid',
      'mny' => 'application/x-msmoney',
      'mov' => 'video/quicktime',
      'movie' => 'video/x-sgi-movie',
      'mp2' => 'video/mpeg',
      'mp3' => 'audio/mpeg',
      'mp4' => 'video/mp4',
      'mpa' => 'video/mpeg',
      'mpe' => 'video/mpeg',
      'mpeg' => 'video/mpeg',
      'mpg' => 'video/mpeg',
      'mpp' => 'application/vnd.ms-project',
      'mpv2' => 'video/mpeg',
      'ms' => 'application/x-troff-ms',
      'msg' => 'application/vnd.ms-outlook',
      'mvb' => 'application/x-msmediaview',
      'nc' => 'application/x-netcdf',
      'nws' => 'message/rfc822',
      'oda' => 'application/oda',
      'p10' => 'application/pkcs10',
      'p12' => 'application/x-pkcs12',
      'p7b' => 'application/x-pkcs7-certificates',
      'p7c' => 'application/x-pkcs7-mime',
      'p7m' => 'application/x-pkcs7-mime',
      'p7r' => 'application/x-pkcs7-certreqresp',
      'p7s' => 'application/x-pkcs7-signature',
      'pbm' => 'image/x-portable-bitmap',
      'pdf' => 'application/pdf',
      'pfx' => 'application/x-pkcs12',
      'pgm' => 'image/x-portable-graymap',
      'pko' => 'application/ynd.ms-pkipko',
      'pma' => 'application/x-perfmon',
      'pmc' => 'application/x-perfmon',
      'pml' => 'application/x-perfmon',
      'pmr' => 'application/x-perfmon',
      'pmw' => 'application/x-perfmon',
      'pnm' => 'image/x-portable-anymap',
      'pot' => 'application/vnd.ms-powerpoint',
      'ppm' => 'image/x-portable-pixmap',
      'pps' => 'application/vnd.ms-powerpoint',
      'ppt' => 'application/vnd.ms-powerpoint',
      'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
      'prf' => 'application/pics-rules',
      'ps' => 'application/postscript',
      'pub' => 'application/x-mspublisher',
      'qt' => 'video/quicktime',
      'ra' => 'audio/x-pn-realaudio',
      'ram' => 'audio/x-pn-realaudio',
      'ras' => 'image/x-cmu-raster',
      'rgb' => 'image/x-rgb',
      'rm' => 'application/vnd.rn-realmedia',
      'rmi' => 'audio/mid',
      'rmvb' => 'application/vnd.rn-realmedia',
      'roff' => 'application/x-troff',
      'rtf' => 'application/rtf',
      'rtx' => 'text/richtext',
      'scd' => 'application/x-msschedule',
      'sct' => 'text/scriptlet',
      'setpay' => 'application/set-payment-initiation',
      'setreg' => 'application/set-registration-initiation',
      'sh' => 'application/x-sh',
      'shar' => 'application/x-shar',
      'sit' => 'application/x-stuffit',
      'snd' => 'audio/basic',
      'spc' => 'application/x-pkcs7-certificates',
      'spl' => 'application/futuresplash',
      'src' => 'application/x-wais-source',
      'sst' => 'application/vnd.ms-pkicertstore',
      'stl' => 'application/vnd.ms-pkistl',
      'stm' => 'text/html',
      'sv4cpio' => 'application/x-sv4cpio',
      'sv4crc' => 'application/x-sv4crc',
      'svg' => 'image/svg+xml',
      'swf' => 'application/x-shockwave-flash',
      't' => 'application/x-troff',
      'tar' => 'application/x-tar',
      'tcl' => 'application/x-tcl',
      'tex' => 'application/x-tex',
      'texi' => 'application/x-texinfo',
      'texinfo' => 'application/x-texinfo',
      'tgz' => 'application/x-compressed',
      'tif' => 'image/tiff',
      'tiff' => 'image/tiff',
      'tr' => 'application/x-troff',
      'trm' => 'application/x-msterminal',
      'tsv' => 'text/tab-separated-values',
      'txt' => 'text/plain',
      'uls' => 'text/iuls',
      'ustar' => 'application/x-ustar',
      'vcf' => 'text/x-vcard',
      'vrml' => 'x-world/x-vrml',
      'wav' => 'audio/x-wav',
      'wcm' => 'application/vnd.ms-works',
      'wdb' => 'application/vnd.ms-works',
      'wks' => 'application/vnd.ms-works',
      'wmf' => 'application/x-msmetafile',
      'wmv' => 'video/x-ms-wmv',
      'wps' => 'application/vnd.ms-works',
      'wri' => 'application/x-mswrite',
      'wrl' => 'x-world/x-vrml',
      'wrz' => 'x-world/x-vrml',
      'xaf' => 'x-world/x-vrml',
      'xbm' => 'image/x-xbitmap',
      'xla' => 'application/vnd.ms-excel',
      'xlc' => 'application/vnd.ms-excel',
      'xlm' => 'application/vnd.ms-excel',
      'xls' => 'application/vnd.ms-excel',
      'xlt' => 'application/vnd.ms-excel',
      'xlw' => 'application/vnd.ms-excel',
      'xof' => 'x-world/x-vrml',
      'xpm' => 'image/x-xpixmap',
      'xwd' => 'image/x-xwindowdump',
      'z' => 'application/x-compress',
      'zip' => 'application/zip',
    );
    
    /**
     * 文件元数据支持的属性名数组。
     * @var array
     */
    private static $meta_keys = array(
        'meta_expiry', //文件过期时间
    );
    
    /**
     * 通过调用 API 接口，将本地文件存储到存储服务中。
     *
     * @param string $source 原始文件所在位置
     * @param string $filename 文件名
     * @param string $mimetype 文件的 MIME type 类型。
     * @param boolean $allow_duplicate 是否允许文件重复。
     * @param array $meta 文件元数据属性设置，目前支持的元数据属性名有：
     *                      meta_expiry: 文件过期时间，单位为秒，即多少秒之后文件过期
     * @return array|boolean 返回此文件在存储中的信息，如果返回 false 则表示有错误，可通过 getErrorCode() 和 getErrorMessge() 获取错误信息，错误的额外数据可以通过 getExtraData() 获取。
     */
    public function putFile($source, $filename, $mimetype, $allow_duplicate = true, $meta = array())
    {
        if (!file_exists($source)) throw new Exception('原始文件不存在（$source: '.$source.'）');
        $data = array(
            'action'   => 'storage.put_file',
            'file'     => curl_file_create($source), //文件路径前加上“@”符号
            'filename' => $filename,
            'filetype' => $mimetype,
            'allow_duplicate' => $allow_duplicate ? 1 : 0,
        );
        $data = $this->addMeta($data, $meta);
        $result = $this->callAPI($data);
        return ($result === false) ? false : $result['fileinfo'];
    }
    
    /**
     * 获取上传文件表单的 POST 参数数组，包括签名等信息。
     *
     * @param integer $expires 签名过期时间，单位为秒。
     * @param string $format 接口返回值的格式，可以是 json 或 redirect，redirect 表示直接进行 url 跳转，必须同时设置 $redirect_url 参数。
     * @param boolean $allow_duplicate 是否允许文件重复。
     * @param string $redirect_url 当参数 $format 为 redirect 时，此参数为返回值回调跳转的 url 地址。
     * @param array $meta 文件元数据属性设置，目前支持的元数据属性名有：
     *                      meta_expiry: 文件过期时间，单位为秒，即多少秒之后文件过期
     * @return array
     */
    public function getUploadPostParams($expires, $format, $allow_duplicate, $redirect_url = '', $meta = array())
    {
        $params = array(
            'action'  => 'storage.post_file',
            'api_key' => $this->api_key,
            'expires' => date('Y-m-d H:i:s', time()+$expires),
            'format'  => $format.'',
            'allow_duplicate' => $allow_duplicate ? 1 : 0,
            'redirect_url' => $redirect_url.'',
        );
        if ($this->debug) $params['debug'] = 1; //打开调试开关
        $params = $this->addMeta($params, $meta);
        $params['signature'] = parent::getSignatureHash($params, $this->api_secret);
        return $params;
    }
    
    /**
     * 通过调用 API 接口，删除存储服务中的文件。
     *
     * @param string|array $filekey 文件唯一标识，如果需要删除多个文件，可以传递由多个 filekey 组成的数组。
     * @return boolean 成功返回 true，返回 false 则表示有错误，可通过 getErrorCode() 和 getErrorMessge() 获取错误信息。
     */
    public function deleteFile($filekey)
    {
        if (is_array($filekey)) $filekey = implode(',', $filekey);
        $data = array(
            'action'  => 'storage.delete_file',
            'filekey' => $filekey,
        );
        $result = $this->callAPI($data);
        return ($result === false) ? false : (boolean)$result['result'];
    }
    
    /**
     * 获取存储服务中的文件信息。
     *
     * @param string $filekey 文件唯一标识
     * @return array|boolen 返回此文件信息，返回 false 则表示有错误，可通过 getErrorCode() 和 getErrorMessge() 获取错误信息。
     */
    public function getFileInfo($filekey)
    {
        $data = array(
            'action'  => 'storage.get_file_info',
            'filekey' => $filekey,
        );
        $result = $this->callAPI($data);
        return ($result === false) ? false : $result['fileinfo'];
    }

    /**
     * 复制存储中的文件。
     *
     * @param string $filekey 复制的源文件唯一标识
     * @return array|boolen 返回新的文件信息，返回 false 则表示有错误，可通过 getErrorCode() 和 getErrorMessge() 获取错误信息。
     */
    public function copyFile($filekey)
    {
        $data = array(
            'action'  => 'storage.copy_file',
            'filekey' => $filekey,
        );
        $result = $this->callAPI($data);
        return ($result === false) ? false : $result['fileinfo'];
    }
    
    /**
     * 设置存储服务中文件的元数据。
     *
     * @param string|array $filekey 文件唯一标识，如果需要删除多个文件，可以传递由多个 filekey 组成的数组。
     * @param array $meta 文件元数据属性设置，目前支持的元数据属性名有：
     *                      meta_expiry: 文件过期时间，单位为秒，即多少秒之后文件过期
     * @return boolean 成功返回 true，返回 false 则表示有错误，可通过 getErrorCode() 和 getErrorMessge() 获取错误信息。
     */
    public function setFileMeta($filekey, $meta)
    {
        if (is_array($filekey)) $filekey = implode(',', $filekey);
        $data = array(
            'action'  => 'storage.set_file_meta',
            'filekey' => $filekey,
        );
        $data = $this->addMeta($data, $meta);
        if (!count($meta)) throw new Exception('元数据参数 meta 不能为空。');
        $result = $this->callAPI($data);
        return ($result === false) ? false : (boolean)$result['result'];
    }
    
    /**
     * 获取文件的 mime type 信息。
     *
     * @param string $filename 文件完整路径。
     * @return string 返回 mime type 信息。
     */
    public static function getFileMimeType($filename)
    {
        $ext = parent::getExt($filename);
        if (isset(self::$mime_types[$ext])) {
            $mime = self::$mime_types[$ext];
        } else if (is_executable(self::BIN_FILE)) {
            $mime = @exec(self::BIN_FILE.' -bi "'.$filename.'"');
        } else if (function_exists('finfo_open')) {
            $finfo = finfo_open(FILEINFO_MIME_TYPE);
            $mime = @finfo_file($finfo, $filename);
            finfo_close($finfo);
        } else if (function_exists('mime_content_type')) {
            $mime = @mime_content_type($filename);
        } else {
            $mime = '';
        }
        return $mime;
    }
    
    /**
     * 从存储服务文件的 URL 中提取出该文件对应的 filekey。
     *
     * @param string $url 存储服务中某一文件的 URL。
     * @return string|boolean 成功返回 filekey 字符串，失败则返回 false。
     */
    public static function getFileKeyFromUrl($url)
    {
        if (preg_match('#/?([a-z0-9]+\.[a-z0-9]+)$#i', trim($url), $matches)) {
            return $matches[1];
        } else {
            return false;
        }
    }
    
    /**
     * 检测文件元数据属性名是否合法。
     *
     * @param string $meta_key 文件元数据属性名。
     * @return boolean 合法返回 true，不合法返回 false。
     */
    public static function isMetaKeyValid($meta_key)
    {
        return in_array($meta_key, self::$meta_keys);
    }
    
    /**
     * 获取所有合法的元数据属性名。
     *
     * @return array
     */
    public static function getValidMetaKeys()
    {
        return self::$meta_keys;
    }
    
    /**
     * 添加文件元数据设置。
     *
     * @param array $data 原有需要传递的参数数据数组。
     * @param array $meta 文件元数据设置数组。
     * @return array
     */
    private function addMeta($data, $meta)
    {
        if (!is_array($meta)) throw new Exception('元数据参数 meta 必须为数组。');
        foreach ($meta as $meta_key => $meta_value) {
            if (!self::isMetaKeyValid($meta_key)) throw new Exception('目前客户端版本不支持元数据属性名“'.$meta_key.'”。');
            if ($meta_key == 'meta_expiry' && is_integer($meta_value) && $meta_value) {
                // 如果文件过期时间为整数，则表示多少秒之后过期，需要将其转换成日期时间类型加到参数中。
                $meta_value = date('Y-m-d H:i:s', time()+$meta_value);
            }
            $data[$meta_key] = $meta_value;
        }
        return $data;
    }
}

if (!function_exists('curl_file_create')) {
    function curl_file_create($filename, $mimetype = '', $postname = '')
    {
        return "@$filename;filename="
        . ($postname ? $postname : basename($filename))
        . ($mimetype ? ";type=$mimetype" : '');
    }
}
